/*
 *  linux/kernel/fork.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 *  'fork.c' contains the help-routines for the 'fork' system call
 * (see also system_call.s), and some misc functions ('verify_area').
 * Fork is rather simple, once you get the hang of it, but the memory
 * management can be a bitch. See 'mm/mm.c': 'copy_page_tables()'
 */
#include <string.h>
#include <errno.h>

#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/segment.h>
#include <asm/system.h>

extern void write_verify(unsigned long address);

long last_pid=0; /* 系统中未使用的进程号 */

// 内核向用户空间中数据页面写操作前检测：若页面是只读的，则执行共享检验和复制页面操作（写时复制）。
// 使用场景：
	// 对于 80386 CPU，在执行特权级0代码时不会理会用户空间中的页面是否是页保护的，因此在执行内核代码时用户空间中数据页面保护标志起不了作用，写时复制机制也就失去了作用。verify_area(） 函数就用于此目的。
	// 但对于 80486 或后来的 CPU，其控制寄存器 CRO 中有一个写保护标志WP（位16），内核可以通过设置该标志来禁止特权级0的代码向用户空间只读页面执行写数据，否则将导致发生写保护异常。从而 486 以上 CPU 可以通过设置该标志来达到本函数的目的。
// 处理逻辑：
	// 该函数对当前进程逻辑地址从 addr 到 addr + size 这一段范围以页为单位执行写操作前的检测操作。
	// 由于检测判断是以页面为单位进行操作，因此程序首先需要找出 addr 所在页面开始地址 start，
	// 然后 start 加上进程数据段基址，使这个 start 变换成 CPU 4G 线性空间中的地址。
	// 最后循环调用 write_verify(）对指定大小的内存空间进行写前验证。
void verify_area(void * addr,int size)
{
	unsigned long start;

	start = (unsigned long) addr;
	// 0xfff大小为4K，start & 0xfff 用来获得指定起始位置 addr（也即 start）在所在页面中的偏移值
	size += start & 0xfff;
	// 获取偏移地址所在页的起始地址，类似于4K对齐
	start &= 0xfffff000;
	// start 加上进程数据段基址，使这个 start 变换成 CPU 线性空间地址
	start += get_base(current->ldt[2]);
	while (size>0) {
		size -= 4096;
		// 验证一页内存
		write_verify(start);
		start += 4096;
	}
}

// 拷贝内存页表（即在线性地址空间中设置新任务 代码段描述符 和 数据段描述符 中的 基址 和 限长，并复制页表。）
// nr：新任务号
// p：新任务结构体指针
int copy_mem(int nr,struct task_struct * p)
{
	unsigned long old_data_base,new_data_base,data_limit;
	unsigned long old_code_base,new_code_base,code_limit;

	code_limit=get_limit(0x0f);  /* 0x0f : 表示从ldt表中获取1号表项，即代码段 */
	data_limit=get_limit(0x17);  /* 0x17 : 表示从ldt表中获取2号表项，即数据段 */
	old_code_base = get_base(current->ldt[1]);
	old_data_base = get_base(current->ldt[2]);
	// 由于 Linux 0. 11 内核还不支持代码和数据段分立的情况，因此这里需要检查代码段和数据段基址和限长是否都分别相同。否则内核显示出错信息，并停止运行。
	if (old_data_base != old_code_base)
		panic("We don't support separate I&D");
	if (data_limit < code_limit)
		panic("Bad data_limit");
	// 1、设置创建中的新进程在线性地址空间中的基地址等于（64MB *其任务号）
	new_data_base = new_code_base = nr * 0x4000000;
	p->start_code = new_code_base; /* 代码段基地址 */
	// 2、设置新进程局部描述符表中 段描述符 中的 基地址
	set_base(p->ldt[1],new_code_base);
	set_base(p->ldt[2],new_data_base);
	// 3、设置（复制）新进程的页目录表项和页表项
		// 即复制当前进程（父进程）的页目录表项和页表项。此时子进程共享父进程的内存页面。
		// 正常情况下 copy_ page_tables()返回0，否则表示出错，则释放刚申请的页表项。
	if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
		printk("free_page_tables: from copy_mem\n");
		free_page_tables(new_data_base,data_limit);
		return -ENOMEM;
	}
	return 0;
}

/*
 *  Ok, this is the main fork-routine. It copies the system process
 * information (task[nr]) and sets up the necessary registers. It
 * also copies the data segment in it's entirety.
 */
// 被系统调用 sys_fork中调用
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
		long ebx,long ecx,long edx,
		long fs,long es,long ds,
		long eip,long cs,long eflags,long esp,long ss)
{
	struct task_struct *p;
	int i;
	struct file *f;

	// 获取空闲页的物理地址(在内核数据段中申请页？？？)
	p = (struct task_struct *) get_free_page();
	if (!p)
		return -EAGAIN;
	task[nr] = p;
	
	// NOTE!: the following statement now work with gcc 4.3.2 now, and you
	// must compile _THIS_ memcpy without no -O of gcc.#ifndef GCC4_3
	// 把当前进程任务结构内容复制到刚申请到的内存页面p开始处。
	*p = *current;	/* NOTE! this doesn't copy the supervisor stack */
	// 修改子进程的结构体成员
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p; /* 任务内核态栈指针esp0：为p所在页的顶端 */
	p->tss.ss0 = 0x10;  /* 内核数据段 */
	p->tss.eip = eip;
	p->tss.eflags = eflags;
	p->tss.eax = 0;  /* 类似于fork的返回值为0，表示是子进程的返回值 */
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff; /* 只取低16位 */
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr); /* （ldt描述符的）段选择子 */
	p->tss.trace_bitmap = 0x80000000; /* I/O 比特位图中的每 1 比特对应1个I/O 端口.高16位有效 。Linux0.11未使用*/
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	// 为新任务拷贝内存页表
	// 即在线性地址空间中设置新任务代码段和数据段描述符中的基址和限长，并复制页表。
	if (copy_mem(nr,p)) {
		// 如果出错（返回值不是0），则复位任务数组中相应项并释放为该新任务分配的用于任务结构的内存页。
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	// 如果父进程中有文件是打开的，则将对应文件的打开次数增1。因为这里创建的子进程会与父进程共享这些打开的文件。
	for (i=0; i<NR_OPEN;i++)
		if ((f=p->filp[i]))
			f->f_count++;
	// 将当前进程（父进程）的 pwd, root 和 executable 引用次数均增1。与上面同样的道理，子进程也引用了这些i节点。
	if (current->pwd)
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	// 在 GDT 表中设置新任务 TSS 段和 LDT 段描述符项
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case */
	// 父进程返回子进程的pid
	return last_pid;
}

// 为新进程取得未使用的进程号 last_pid(last_pid 是一个全局变量，不用返回)。函数返回在任务数组中的任务号(数组索引）。
int find_empty_process(void)
{
	int i;

	repeat:
		// 首先获取新的进程号。如果 last_pid 增1后超出进程号的正数表示范围，则重新从1开始使用 pid 号。
		if ((++last_pid)<0) last_pid=1;
		// 然后在任务数组中搜索刚设置的 pid 号是否已经被任何任务使用。如果是，跳转到函数开始处重新获得一个pid号。
		for(i=0 ; i<NR_TASKS ; i++)
			if (task[i] && task[i]->pid == last_pid) goto repeat;
	// 接着在任务数组中为新任务寻找一个空闲项，并返回项号。
	for(i=1 ; i<NR_TASKS ; i++)
		if (!task[i])
			return i;
	// 如果此时任务数组中64 个项已经被全部占用，则返回出错码。
	return -EAGAIN;
}
